package ece.edt;

import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.httpclient.Cookie;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.HttpState;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.cookie.CookiePolicy;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.log4j.Logger;

import ece.edt.exceptions.CASClientException;
import ece.edt.exceptions.HyperPlanningMethodException;

/**
 * @author laurent.virot
 * Extend HttpClient @see org.apache.commons.httpclient.HttpClient capabilities to CAS
 */
public class CASClient extends HttpClient{
	private static final Logger log = Logger.getLogger(CASClient.class);
	private final String fCasUrl;
	private final String fServiceUrl;
	private final String fUsername;
	private final String fPassword;
	private static CASClient casClient;
	
	/**
	 * Constructor. Set up CAS parameters. 
	 * @param casBaseUrl Url of the CAS authentification site. Something likes : https://webauth.site.com/cas/login
	 * @param serviceUrl Url of the service you want to reach. Something like : http://edt.site.com/
	 * @param username CAS login
	 * @param password CAS password
	 * @throws CASClientException
	 */
	protected CASClient(String casBaseUrl, String serviceUrl, String username, String password) throws CASClientException {
		this(casBaseUrl, serviceUrl, username, password, null);
	}
	
	/**
	 * Constructor. Set up CAS parameters. If cookie is given, we initialize the client with its value else we login to CAS.
	 * @param casBaseUrl Url of the CAS authentification site. Something likes : https://webauth.site.com/cas/login
	 * @param serviceUrl Url of the service you want to reach. Something like : http://edt.site.com/
	 * @param username CAS login
	 * @param password CAS password
	 * @param cookie Value of MODCASID cookie.
	 * @throws CASClientException
	 */
	protected CASClient(String casBaseUrl, String serviceUrl, String username, String password, String cookie) throws CASClientException {
		fCasUrl = casBaseUrl;
		fServiceUrl = serviceUrl;
		fUsername = username;
		fPassword = password;
		
		// Ensure the cookie will be send in the good format.
		getParams().setCookiePolicy(CookiePolicy.BROWSER_COMPATIBILITY);
		
		if(cookie != null){
			// Extract host (remove 'http://' and last '/' from serviceURL).
			String host = serviceUrl.split("http://")[1];
			host = host.substring(0, host.length()-1);
			
			HttpState initialState = new HttpState();
			Cookie modcasid = new Cookie(host, "MODCASID", cookie, "/", null, false);
			initialState.addCookie(modcasid);
			setState(initialState);
		}else{
			authenticate();
		}
	}
	
	/**
	 * Get a CASClient instance. Ensure only one instance could be created.
	 * @param casBaseUrl Url of the CAS authentification site. Something likes : https://webauth.site.com/cas/login
	 * @param serviceUrl Url of the service you want to reach. Something like : http://edt.site.com/
	 * @param username CAS login
	 * @param password CAS password
	 * @param cookie Value of MODCASID cookie.
	 * @return CASClient instance
	 * @throws CASClientException
	 */
	public static CASClient getInstance(String casBaseUrl, String serviceUrl, String username, String password, String cookie) throws CASClientException{
		CASClient instance = null;
		if(casClient != null){
			instance = casClient;
		}else{
			instance = new CASClient(casBaseUrl, serviceUrl, username, password, cookie);
		}
		return instance;
	}
	
	/**
	 * Get a CASClient instance. Ensure only one instance could be created.
	 * @param casBaseUrl Url of the CAS authentification site. Something likes : https://webauth.site.com/cas/login
	 * @param serviceUrl Url of the service you want to reach. Something like : http://edt.site.com/
	 * @param username CAS login
	 * @param password CAS password
	 * @return CASClient instance
	 * @throws CASClientException
	 */
	public static CASClient getInstance(String casBaseUrl, String serviceUrl, String username, String password) throws CASClientException{
		CASClient instance = null;
		if(casClient != null){
			instance = casClient;
		}else{
			instance = new CASClient(casBaseUrl, serviceUrl, username, password);
		}
		return instance;
	}
	/**
	 * Connect to CAS, authentificate and check the MODCASID cookie is present.
	 * @throws CASClientException
	 */
	private void authenticate() throws CASClientException {
		// Get a preconfigured method.

		HttpState initialState = new HttpState();
		setState(initialState);
		
		HttpMethod method = initAuth();
		
		try {
			// Connect to CAS with preconfigured method.
			switch(super.executeMethod(method)){
			    // No redirection. Something went wrong, abording.
				case HttpStatus.SC_OK :
					method.releaseConnection();
					throw new CASClientException("Bad login/password.");
					
				// Redirected	
				case HttpStatus.SC_MOVED_TEMPORARILY:
					Header locationHeader = method.getResponseHeader("location");
					if (locationHeader == null) {
						method.releaseConnection();
						throw new CASClientException("Could not get redirection.");
					}
					method.releaseConnection();
					
					// Connect to the redirection url.
					method = new GetMethod(locationHeader.getValue());
					super.executeMethod(method);

					// Check MODCASID cookie is here.
					for(Cookie cookie : getState().getCookies()){
						if(cookie.getName().equals("MODCASID")){
							// Something went wrong.
							if(cookie.getValue() == null){
								method.releaseConnection();
								throw new CASClientException("Could not get MODCASID");
							}
							break;
						}
					}
					break;
					
				// Something went wrong.
				default:
					throw new CASClientException("Could not get service ticket.");
			}
		} catch (HttpException e) {
			throw new CASClientException(e);
		} catch (IOException e) {
			throw new CASClientException(e);
		} finally{
			method.releaseConnection();
		}
	}

	/**
	 * Initialize the authentification by connecting to the CAS, getting the login ticket and
	 * setting up the a HttpMethod with username, password, service, login ticket and some other values.
	 * @return a HttpMethod configurated with username, password, service, login ticket and some other values.
	 * @throws CASClientException
	 */
	private HttpMethod initAuth() throws CASClientException {
		PostMethod method = new PostMethod(fCasUrl);
		try {
			// Connect to CAS.
			int statusCode = super.executeMethod(method);
			
			// Check if HTTP response is OK.
			if (statusCode != HttpStatus.SC_OK) {
				method.releaseConnection();
				throw new CASClientException("Could not connect to CAS: " + method.getStatusLine());
			}
			
			// Extract the login ticket (lt) from the html response.
			Pattern pattern = Pattern.compile("<input type=\"hidden\" name=\"lt\" value=\"(.+)\"");
			Matcher matcher = pattern.matcher(method.getResponseBodyAsString());
			
			// If nothing matches, abording.
			if (!matcher.find()) {
				method.releaseConnection();
				throw new CASClientException("Could not obtain Login Ticket token from CAS.");
			}
			method.releaseConnection();
			
			// Set up the method with username, password, service, login ticket and some other values.
			method = new PostMethod(fCasUrl);
			method.setParameter("username", fUsername);
			method.setParameter("password", fPassword);
			method.setParameter("service", fServiceUrl);
			method.setParameter("_currentStateId", "viewLoginForm");
			method.setParameter("_eventId", "submit");
			method.setParameter("lt", matcher.group(1));
		} catch (HttpException e) {
			throw new CASClientException(e.getMessage());
		} catch (IOException e) {
			throw new CASClientException(e.getMessage());
		}
		
		return method;
	}
	
    /**
     * @see org.apache.commons.httpclient.HttpClient#executeMethod(org.apache.commons.httpclient.HttpMethod)
     */
    @Override
	public int executeMethod(HttpMethod method) throws IOException, HttpException {
    	// Permit to detect redirection.
    	method.setFollowRedirects(false);
    	
    	// Execute the method.
    	int status = super.executeMethod(method);
    	
    	// Response is not OK. Cookie has probably expired.
    	if(status != HttpStatus.SC_OK){
	    	// Try to reauthentificate and to reexecute method.
	    	try {
				authenticate();
				super.executeMethod(method);
			} catch (CASClientException e) {
				log.error(e, e);
			}
    	}
    	
    	// If we are dealing with a HyperPlanningMethod...
    	if(method instanceof HyperPlanningMethod){
    		// ... we launch a routine to decrypt it.
	    	try {
				((HyperPlanningMethod) method).decrypt();
			} catch (HyperPlanningMethodException e) {
				log.error(e);
			}
    	}
    	
    	return status;
	}

	/**
	 * @return ServiceUrl field
	 */
	public String getServiceUrl() {
		return fServiceUrl;
	}
}
